import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { PageHeader, Section, KV, Note } from '@/components/ui/section'; import { EmptyState } from '@/components/ui/empty-state'; import { Freshness } from '@/components/ui/freshness'; import { Badge, ClaimBadge, ConfidenceBadge } from '@/components/ui/badge'; import { SourceBadge } from '@/components/ui/source-badge'; import { Breadcrumbs } from '@/components/layout/breadcrumbs'; import { CountryActivity } from '@/components/country/activity'; import { TrendChart, type TrendSeries } from '@/components/charts/trend-chart'; import { getGeographyBySlug, coverageFor, yearsFor, allSitesObservations, topCancersFor, trendFor, geographyScopeCode, WHO_REGION_LABEL, SEXES, BURDEN_METRICS, type Sex, type TopCancersResult } from '@/lib/queries/geography'; import { EPI_METRIC_LABEL } from '@/lib/queries/epidemiology'; import { loadProvenance, toInfo } from '@/lib/queries/provenance'; import { listSources } from '@/lib/queries/sources'; import { jsonLd } from '@/lib/seo'; import { SITE_URL, SITE_NAME } from '@/lib/site'; import { str, int, oneOf, type SP } from '@/lib/search-params'; import { fmtInt, fmtValue, humanize, toDate, unitLabel } from '@/lib/format'; export const revalidate = 3600; type Params = { slug: string }; export async function generateMetadata({ params }: { params: Promise }): Promise { const geo = await getGeographyBySlug((await params).slug); if (!geo) return { title: 'Not found' }; return { title: `${geo.name} — cancer statistics`, description: `Cancer incidence and mortality observations for ${geo.name}: top cancers by annual deaths, new cases and age-standardized rates per year and sex, with trends and sources.`, alternates: { canonical: `/country/${geo.slug}` }, }; } /** * /country/[slug] (§47): header, latest-year summary, top-cancer tables per metric with sex/year selectors, * a multi-line trend of the top 8 cancers' age-standardized mortality, and a sources/freshness block. * Every value comes from epidemiology_observations; ranks from the matching ranking snapshot when it exists. */ export default async function CountryPage({ params, searchParams }: { params: Promise; searchParams: Promise }) { const { slug } = await params; const sp = await searchParams; const geo = await getGeographyBySlug(slug); if (!geo) notFound(); const [coverage, years, sources] = await Promise.all([coverageFor(geo.id), yearsFor(geo.id), listSources()]); const latestYear = years[0]; const sex = oneOf(sp, 'sex', SEXES, 'all'); const year = latestYear ? int(sp, 'year', latestYear, years[years.length - 1]!, latestYear) : null; const scopeCode = geographyScopeCode(geo); const crumbs = [{ label: 'Countries', href: '/countries' }, ...(geo.parent_slug && geo.parent_name && geo.kind === 'subdivision' ? [{ label: geo.parent_name, href: `/country/${geo.parent_slug}` }] : []), { label: geo.name }]; if (!latestYear || year == null) { return (
Country statistics appear once a licensed registry connector has ingested observations for {geo.name}. IARC / GLOBOCAN (185 countries) is under license review and the SEER API awaits credentials.
Geographies with data
); } const [allSites, ...tops] = await Promise.all([allSitesObservations(geo.id, year, sex), ...BURDEN_METRICS.map((m) => topCancersFor(geo, m, year, sex, 40))]); const byMetric = new Map(tops.map((t) => [t.metric, t])); const asmr = byMetric.get('as_mortality_rate'); const trendIds = (asmr?.rows.length ? asmr : byMetric.get('mortality_count'))?.rows.slice(0, 8).map((r) => r.cancer_id) ?? []; const trendMetric = asmr?.rows.length ? 'as_mortality_rate' : 'mortality_count'; const trend = await trendFor(geo.id, trendMetric, sex, trendIds); const provIds = tops.flatMap((t) => t.rows.slice(0, 1).map((r) => r.provenance_id)); const prov = await loadProvenance([...provIds, ...allSites.map((a) => a.provenance_id)]); const usedSources = [...new Set(coverage.map((c) => c.source_slug))].map((s) => sources.find((x) => x.slug === s)).filter((s): s is NonNullable => !!s); const freshest = coverage.map((c) => toDate(c.last_updated)).filter((d): d is Date => !!d).sort((a, b) => b.getTime() - a.getTime())[0] ?? null; const nCancers = Math.max(...coverage.map((c) => c.n_cancers), 0); const estimateTypes = [...new Set(coverage.flatMap((c) => c.estimate_types))]; const standardPop = coverage.find((c) => c.standard_population)?.standard_population ?? null; const ld = { '@context': 'https://schema.org', '@type': 'Dataset', name: `Cancer incidence and mortality observations — ${geo.name}`, description: `Per-site cancer observations for ${geo.name} (${years[years.length - 1]}–${latestYear}): annual deaths, new cases and age-standardized rates by sex, as published by ${usedSources.map((s) => s.name).join('; ') || 'the source registry'}. Normalized by CancerIndex without changing values.`, url: `${SITE_URL}/country/${geo.slug}`, creator: { '@type': 'Organization', name: SITE_NAME, url: SITE_URL }, isBasedOn: usedSources.map((s) => s.homepage ?? `${SITE_URL}/source/${s.slug}`), spatialCoverage: { '@type': 'Place', name: geo.name, ...(geo.iso3 ? { identifier: geo.iso3 } : {}) }, temporalCoverage: `${years[years.length - 1]}/${latestYear}`, variableMeasured: [...new Set(coverage.map((c) => EPI_METRIC_LABEL[c.metric] ?? c.metric))], license: usedSources.map((s) => s.license).filter(Boolean).join('; ') || undefined, ...(freshest ? { dateModified: freshest.toISOString() } : {}), }; const q = (over: Partial<{ sex: string; year: number }>) => { const p = new URLSearchParams(); const s = over.sex ?? sex; const y = over.year ?? year; if (s !== 'all') p.set('sex', s); if (y !== latestYear) p.set('year', String(y)); const qs = p.toString(); return `/country/${geo.slug}${qs ? `?${qs}` : ''}`; }; return (